package jmpo.cs;

public class Switch {
    /**
     * Простейший свитч использует сравнение с целочисленными
     * или символьными константами.
     * Допустимы типы:
     * byte, short, int и char.
     * И их Boxes (обёртки) Byte, Short, Integer, Character.
     */
    private static void simpleSwitch() {
        int value = 5;
        switch(value) {
            case 1: // 1 - проверяемое на совпадение значение
                System.out.println("Case 1");
                break;  // необходимо для выхода из switch
            case 2:
                System.out.println("Case 2");
                break;
            case 3:
                System.out.println("Case 3");
                break;
            case 4:
                System.out.println("Case 4");
                break;
            case 5:
                System.out.println("Case 5");
                break;
            default: // если ни один case не подошёл
                System.out.println("Default case");
        }
    }
    /**
     * switch допускает "проваливает" (fallthrough).
     * Это его поведение по умолчанию.
     * После попадания в какой-то case switch продолжает исполнение, 
     * как если бы весь switch был одним блоком кода.
     * Ниже представлен случай намеренно использующий эту возможность.
     */
    private static void fallthroughSwitch() {
        int monthNumber = 4;
        /**
         * switch подсчитывает количество дней, прошедших с начала года
         * до заданного номером monthNumer месяца. Результат достигается
         * последовательным сложением количества дней в каждом предыдущем 
         * месяце. Сначала мы попадает в нужный case, а затем проваливаемся 
         * в следующие case, которые соответствуют предыдущим месяцам, 
         * складывая итоговое значение.
         */
        int daysCount = 0;
        switch(monthNumber) {
            case 12: // 12 - означает Декабрь. Мы прошли через весь год.
                     // Необходимо просуммировать все месяцы.
                     // Так как этот case стоит первым, то мы пройдём все case
                     // насквозь. Все месяцы просуммируются, как мы и хотим.
                daysCount += 31;
            case 11:
                daysCount += 30;
            case 10:
                daysCount += 31;
            case 9:
                daysCount += 30;
            case 8:
                daysCount += 31;
            case 7:
                daysCount += 31;
            case 6:
                daysCount += 30;
            case 5:
                daysCount += 31;
            case 4:
                daysCount += 30;
            case 3: // 3 - март
                daysCount += 31;
            case 2: // 2 - февраль
                daysCount += 28; // не будем учитывать високосные года
            case 1: // 1 означает, что прошёл только один месяц - Январь
                daysCount += 31; // добавляем за него 31 день
            //  после этого выходим из switch
        }
        System.out.println(daysCount);
    }

    /**
     * Проваливание (fallthrough) позволяет выполнить один блок кода для 
     * нескольких вариантов значений, создав case с пустым statement.
     */
    private static void fallthroughForMultipleVariants() {
        int monthNumber = 4; // 1 - январь, 12 - декабрь
        switch(monthNumber) {
            case 12:
            case 1:
            case 2:
                System.out.println("Winter");
                break;
            case 3:
            case 4:
            case 5:
                System.out.println("Spring");
                break;
            case 6:
            case 7:
            case 8:
                System.out.println("Summer");
                break;
            case 9:
            case 10:
            case 11:
                System.out.println("Fall");
                break;
            default:
                System.out.println("Wrong month number");
        }
    }

    /** 
     * Начиная с Java 14 появилась возможность тоже самое написать 
     * с помощью перечисленя значений через запятую.
     */
    private static void multipleVariantsAlternative() {
        int monthNumber = 4; // 1 - январь, 12 - декабрь
        switch(monthNumber) {
            case 12, 1, 2:
                System.out.println("Winter");
                break;
            case 3, 4, 5:
                System.out.println("Spring");
                break;
            case 6, 7, 8:
                System.out.println("Summer");
                break;
            case 9, 10, 11:
                System.out.println("Fall");
                break;
        } 
    }

    /**
     * Если для некоторого case требуется достаточно сложный код,
     * например включающий объявление переменной,
     * то можно использовать блок кода, ограниченный фигурными скобками.
     * При этом break может располагаться как внутри блока, так и вне его.
     */
    private static void codeblocksInSwitchCase() {
        String operation = "sum";
        int lha = 3, rha = 4;
        switch(operation) {
            case "sum":
                System.out.format("The sum is: %d", lha + rha);
                break;
            case "sub": {
                var diff = lha - rha; // для примера объявления переменной
                System.out.format("The difference is: %d",diff);
                break;
            }
            case "mul":
                System.out.format("The product is: %d", lha * rha);
                break;
            case "div":
            {
                if (0 == rha)
                    System.out.println("Error: division by zero");
                else
                    System.out.format("%d = %d * %d + %d", lha, rha, lha / rha, lha % rha);
            }
            break;
        }
    }

    /**
     * Начиная с Java 7 есть возможность сравниваться не только с числовыми,
     * но и со строковыми константами.
     */
    private static void switchWithStrings() {
        String animal = "Cat"; 
        /**
         * Обратим внимание, что animal - это зануляемая ссылка (nullable reference).
         * В отличие от фундаментальных типов ссылка может быть не связана ни 
         * с каким значением. В этом случае будет выброшено исключение:
         * NullPointerException
         */
        switch(animal) {
            case "Cat":
                System.out.println("Meow!");
                break;
            case "Dog":
                System.out.println("Bark!");
                break;
            case "Bird":
                System.out.println("Tweet!");
                break;
            default:
                System.out.println("Unknown animal");
        }
    }

    /**
     * Можно использовать не только литеральные, но и поименованные
     * константы.
     */
    private static void swithcWithFinals() {
        final String followerRole = "F";
        final String authorRole = "A";
        final String moderatorRole = "M";

        String actualRole = followerRole;
        switch(actualRole) {
            case followerRole:
                System.out.println("The user is a follower");
                break;
            case authorRole:
                System.out.println("The user is an author");
                break;
            case moderatorRole:
                System.out.println("The user is a moderator");
                break;
            default:
                System.out.println("Error user role");
        }
    }

    /**
     * Сравнения можно проводить с элементами специального типа классов - enum, 
     * перечисление.
     */
    private static void switchWithEnum() {
        enum Role {
            FOLLOWER,
            AUTHOR,
            MODERATOR
        }

        Role actualRole = Role.FOLLOWER;
        switch(actualRole) {
            case FOLLOWER:
                System.out.println("The user is a follower");
                break;
            case AUTHOR:
                System.out.println("The user is an author");
                break;
            case MODERATOR:
                System.out.println("The user is a moderator");
                break;
            default:
                System.out.println("Error user role");
        }
    }

    /**
     * В Java 14 появилась новая возможность: создавать выражения со switch.
     * Switch Expressions позволяют вернуть из блока switch значение,
     * которое может быть немедленно далее использовано в выражении.
     * В простейшем случае это можно сделать с помощью ключевого слова
     * yield.
     */
    private static void yieldSwitchExpression() {
        char letter = 'a';
        String typeName = 
            switch(letter) {
                case 'a', 'e', 'i', 'o', 'u', 'y':
                    yield "vowel";
                default:
                    yield "non vowel";
            };
        System.out.println(typeName);
        /**
         * Важно чтобы такое выражение было:
         * 1) исчерпывающим, т.е. возвращало значение для любого входящего параметра
         * 2) все значения должны приводится к одному общему типу
         */
    }

    /** 
     * Если для данного case 
     * единственным результатом является возвращение значения некоторого выражения,
     *  то можно опустить слово yield и использовать специальный синтаксис: 
     * case <value>[,<value>]* -> <expression>;
     */
    private static void onelinecaseSwitchExpression() {
        String operation = "div";
        int lha = 35, rha = 4;
        int result = switch(operation) {
            case "sum" -> lha + rha; // в таком случае break или yield не требуются
            case "sub" -> lha - rha;
            case "mul" -> lha * rha;
            case "div" ->
            {
                if (0 == rha) // вычисления можно прерывать исключениями
                    throw new IllegalArgumentException("Division by zero");
                else
                    yield lha / rha; 
            }
            default -> // нам нужно обработать любое значение строки, это можно сделать только указав default
                throw new IllegalArgumentException(
                    String.format("Unknown operation: %s", operation));

        };
        System.out.format("Result is %d", result);
    }

    /** 
     * Начиная с Java 17 switch позволяет упростить условие с интроспекцией типа.
     *  Например вместо if (a instance of <type>) написать case <type> <ident> ->.
     *  Такой switch называют pattern matching.
     */
    private static void introspectiveSwitchStatement() {
        interface Shape { }
        class Rectangle implements Shape { double w, h; Rectangle(double in_w, double in_h) { w = in_w; h = in_h;} }
        class Circle implements Shape { double r; Circle(double in_r) {r = in_r;}}

        Shape s = new Rectangle(1.0, 2.0);
        switch(s) {
            case Rectangle r:
                System.out.println(r.w * r.h);
                break;
            case Circle c:
                System.out.println(Math.PI * c.r * c.r);
                break;
            default: // такой switch должен быть исчерпывающим, как и switch statement
                throw new IllegalArgumentException("Unknown shape");
        }
    }

    private static void introspectiveSwitchExpression() {
        interface Shape { }
        class Rectangle implements Shape { double w, h; Rectangle(double in_w, double in_h) { w = in_w; h = in_h;} }
        class Circle implements Shape { double r; Circle(double in_r) {r = in_r;}}

        Shape s = new Circle(1.0);
        var area = switch(s) {
            case Rectangle r -> r.w * r.h;
            case Circle c -> Math.PI * c.r * c.r;
            default ->
                throw new IllegalArgumentException("Unknown shape");
        };
        System.out.println(area);
    }

    /** 
     * При анализе типа объекта, на который указывает ссылка, может возникнуть необходимость
     * проверить ссылку на null; чтобы не делать это отдельной проверкой, её можно 
     * произвести в одном из case.
     */
    private static void nullcheckIntrospectiveSwitch() {
        interface Shape { }
        class Rectangle implements Shape { double w, h; Rectangle(double in_w, double in_h) { w = in_w; h = in_h;} }
        class Circle implements Shape { double r; Circle(double in_r) {r = in_r;}}

        Shape s = null;
        double area = switch(s) {
            case Rectangle r -> r.w * r.h;
            case Circle c -> Math.PI * c.r * c.r;
            case null -> 0.0;
            default ->
                throw new IllegalArgumentException("Unknown shape"); 
        };
        System.out.println(area); 
    }

    /**
     * В Java 20 можно проводить уточнения при интроспекции, 
     * добавляя дополнительные условия
     */
    private static void objectIntrospectionWithConditionsSwitch() {
        String s = "Hello World!";
        Integer i = 5;

        Object o = s;
        int k = switch(o) {
            case Integer ii -> ii;
            case String ss when ss.length() > 10 -> 10; // (Java 20)
            // case String ss && ss.length() > 10 -> 10; // (Java 19)
            case String ss -> ss.length();
            default -> 0;
        };
        System.out.println(k);
    }

    public static void main(String[] args) {
        if (false) simpleSwitch();
        if (false) fallthroughSwitch();
        if (false) fallthroughForMultipleVariants();
        if (false) multipleVariantsAlternative();
        if (false) codeblocksInSwitchCase();
        if (false) switchWithStrings();
        if (false) swithcWithFinals();
        if (false) switchWithEnum();
        if (false) yieldSwitchExpression();
        if (false) onelinecaseSwitchExpression();
        if (false) introspectiveSwitchStatement();
        if (false) introspectiveSwitchExpression();
        if (false) nullcheckIntrospectiveSwitch();
        if (false) objectIntrospectionWithConditionsSwitch(); 
    }
}